package com.dbflow5.processor.definition;

import com.dbflow5.MapUtils;
import com.dbflow5.StringUtils;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.MethodDefinition;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.definition.behavior.Behaviors;
import com.dbflow5.processor.definition.behavior.ColumnBehaviors;
import com.dbflow5.processor.definition.column.ColumnAccessor;
import com.dbflow5.processor.definition.column.ColumnDefinition;
import com.dbflow5.processor.definition.column.ReferenceColumnDefinition;
import com.dbflow5.processor.utils.*;
import com.squareup.javapoet.*;

import java.io.IOException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.processing.FilerException;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;

/**
 * Description: Used to write Models and ModelViews
 */
public abstract class EntityDefinition extends BaseDefinition {

    public List<ColumnDefinition> columnDefinitions = new ArrayList<>();

    public List<ColumnDefinition> sqlColumnDefinitions() {
        return columnDefinitions.stream().filter(columnDefinition -> !(columnDefinition.type instanceof ColumnDefinition.Type.RowId)).collect(Collectors.toList());
    }

    public Map<ClassName, List<ColumnDefinition>> associatedTypeConverters = new HashMap<>();
    public Map<ClassName, List<ColumnDefinition>> globalTypeConverters = new HashMap<>();
    public List<ColumnDefinition> packagePrivateList = new ArrayList<>();

    public Map<String, Element> classElementLookUpMap = new HashMap<>();

    public String modelClassName() {
        return typeElement.getSimpleName().toString();
    }

    public ColumnBehaviors.PrimaryKeyColumnBehavior primaryKeyColumnBehavior;

    public DatabaseDefinition databaseDefinition() {
        DatabaseObjectHolder holder = manager.getDatabaseHolderDefinition(associationalBehavior().databaseTypeName);
        if(holder != null) {
            return holder.databaseDefinition;
        }
        throw new RuntimeException("DatabaseDefinition not found for DB element named ${associationalBehavior.name}" +
                " for db type: ${associationalBehavior.databaseTypeName}.");
    }

    public boolean hasGlobalTypeConverters() {
        return !globalTypeConverters.isEmpty();
    }

    public boolean implementsLoadFromCursorListener() {
        return ProcessorUtils.implementsClass(typeElement, manager.processingEnvironment, ClassNames.LOAD_FROM_CURSOR_LISTENER);
    }

    public EntityDefinition(TypeElement typeElement, ProcessorManager processorManager) {
        super(typeElement, processorManager);
    }

    public abstract Behaviors.AssociationalBehavior associationalBehavior();

    public abstract Behaviors.CursorHandlingBehavior cursorHandlingBehavior();

    public ColumnBehaviors.PrimaryKeyColumnBehavior primaryKeyColumnBehavior() {
        if(primaryKeyColumnBehavior == null) {
            primaryKeyColumnBehavior = new ColumnBehaviors.PrimaryKeyColumnBehavior(false, null, false);
        }
        return primaryKeyColumnBehavior;
    }

    public abstract MethodDefinition[] methods();

    protected abstract void createColumnDefinitions(TypeElement typeElement);

    public abstract List<ColumnDefinition> primaryColumnDefinitions();

    public void prepareForWrite() {
        classElementLookUpMap.clear();
        columnDefinitions.clear();
        packagePrivateList.clear();

        prepareForWriteInternal();
    }

    protected abstract void prepareForWriteInternal();

    public TypeName parameterClassName() {
        return elementClassName;
    }

    public String addColumnForCustomTypeConverter(ColumnDefinition columnDefinition, ClassName typeConverterName) {
        List<ColumnDefinition> columnDefinitions = MapUtils.getOrPut(associatedTypeConverters, typeConverterName, new ArrayList<>());
        columnDefinitions.add(columnDefinition);
        return "typeConverter" + typeConverterName.simpleName();
    }

    public String addColumnForTypeConverter(ColumnDefinition columnDefinition, ClassName typeConverterName) {
        List<ColumnDefinition> columnDefinitions = MapUtils.getOrPut(globalTypeConverters, typeConverterName, new ArrayList<>());
        columnDefinitions.add(columnDefinition);
        return "global_typeConverter" + typeConverterName.simpleName();
    }

    public void writeConstructor(TypeSpec.Builder builder) {
        Methods.CustomTypeConverterPropertyMethod customTypeConverterPropertyMethod = new Methods.CustomTypeConverterPropertyMethod(this);
        customTypeConverterPropertyMethod.addToType(builder);

        constructor(builder, builder1 -> {
            if (hasGlobalTypeConverters()) {
                builder1.addParameter(ParameterSpec.builder(ClassNames.DATABASE_HOLDER, "holder").build());
            }
            builder1.addParameter(ParameterSpec.builder(ClassNames.BASE_DATABASE_DEFINITION_CLASSNAME, "databaseDefinition").build());
            builder1.addModifiers(Modifier.PUBLIC);
            builder1.addStatement("super(databaseDefinition)");
            CodeBlock.Builder codeBuilder = customTypeConverterPropertyMethod.addCode(CodeBlock.builder());
            builder1.addCode(codeBuilder.build());
            return builder1;
        });
    }

    private TypeSpec.Builder constructor(TypeSpec.Builder builder, Function<MethodSpec.Builder, MethodSpec.Builder> methodSpecFunction, ParameterSpec.Builder... parameters) {
        List<ParameterSpec> parameterSpecs = new ArrayList<>();
        if (parameters != null) {
            for (ParameterSpec.Builder it : parameters) {
                parameterSpecs.add(it.build());
            }
        }
        return builder.addMethod(methodSpecFunction.apply(MethodSpec.constructorBuilder()).addParameters(parameterSpecs).build());
    }

    public TypeSpec.Builder writeGetModelClass(TypeSpec.Builder typeBuilder, ClassName modelClassName) {
        return JavaPoetExtensions.overrideFun(typeBuilder, ParameterizedTypeName.get(ClassName.get(Class.class), modelClassName), "getTable", new Function<MethodSpec.Builder, Void>() {
            @Override
            public Void apply(MethodSpec.Builder builder) {
                builder.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
                builder.addStatement("$T.class", modelClassName);
                return null;
            }
        });
    }

    public void writePackageHelper(ProcessingEnvironment processingEnvironment) throws IOException {
        int count = 0;

        if (!packagePrivateList.isEmpty()) {
            TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(elementClassName.simpleName() + "_Helper")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

            for (ColumnDefinition columnDefinition : packagePrivateList) {
                String helperClassName = ElementExtensions.getPackage(columnDefinition.element, ProcessorManager.manager) + "." + ElementExtensions.toClassName(columnDefinition.element.getEnclosingElement(), ProcessorManager.manager).simpleName() + "_Helper";
                if (columnDefinition instanceof ReferenceColumnDefinition) {
                    TableDefinition tableDefinition = databaseDefinition().objectHolder.tableDefinitionMap.get(((ReferenceColumnDefinition) columnDefinition).referencedClassName);
                    if (tableDefinition != null) {
                        helperClassName = ElementExtensions.getPackage(tableDefinition.element, ProcessorManager.manager) + "."+ClassName.get((TypeElement)tableDefinition.element).simpleName()+"_Helper";
                    }
                }
                ClassName className = ElementUtility.getClassName(helperClassName, manager);

                if (className != null && ColumnAccessor.PackagePrivateScopeColumnAccessor.containsColumn(className, columnDefinition.columnName)) {
                    boolean samePackage = ElementUtility.isInSamePackage(manager, columnDefinition.element, element);
                    String methodName = StringUtils.capitalize(columnDefinition.columnName);

                    MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("get" + methodName).addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                            .returns(columnDefinition.elementTypeName)
                            .addParameter(ParameterSpec.builder(elementTypeName, ModelUtils.variable).build());

                    if (samePackage) {
                        methodSpecBuilder.addStatement(ModelUtils.variable + "." + columnDefinition.elementName);
                    } else {
                        methodSpecBuilder.addStatement("$T.get" + methodName + "(" + ModelUtils.variable + ")", className);
                    }

                    typeBuilder.addMethod(methodSpecBuilder.build());

                    MethodSpec.Builder methodSpecBuilder2 = MethodSpec.methodBuilder("set" + methodName).addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                            .returns(TypeName.VOID)
                            .addParameter(ParameterSpec.builder(elementTypeName, ModelUtils.variable).build())
                            .addParameter(ParameterSpec.builder(columnDefinition.elementTypeName, "var").build());

                    if (samePackage) {
                        methodSpecBuilder2.addStatement(ModelUtils.variable + "." + columnDefinition.elementName + " = var");
                    } else {
                        methodSpecBuilder2.addStatement("$T.set" + methodName + "(" + ModelUtils.variable + ", var)", className);
                    }
                    typeBuilder.addMethod(methodSpecBuilder2.build());

                    count++;
                } else if (className == null) {
                    manager.logError(EntityDefinition.class, "Could not find classname for: " + helperClassName);
                }
            }

            // only write class if we have referenced fields.
            if (count > 0) {
                JavaFile.Builder javaFileBuilder = JavaFile.builder(packageName, typeBuilder.build());
                javaFileBuilder.build().writeTo(processingEnvironment.getFiler());
            }
        }
    }

    /**
     * Do not support inheritance on package private fields without having ability to generate code for it in
     * same package.
     *
     * @param isPackagePrivateNotInSamePackage isPackagePrivateNotInSamePackage
     * @param element element
     * @return check in package private fields
     */
    public boolean checkInheritancePackagePrivate(boolean isPackagePrivateNotInSamePackage, Element element) {
        if (isPackagePrivateNotInSamePackage && !manager.elementBelongsInTable(element)) {
            manager.logError("Package private inheritance on non-table/querymodel/view " +
                    "is not supported without a @InheritedColumn annotation." +
                    " Make $element from ${element.enclosingElement} public or private.");
            return true;
        }
        return false;
    }

    public static void safeWritePackageHelper(Collection<? extends EntityDefinition> collection, ProcessorManager processorManager) throws IOException {
        try {
            for (EntityDefinition definition : collection) {
                definition.writePackageHelper(processorManager.processingEnvironment);
            }
        } catch (FilerException e) { /*Ignored intentionally to allow multi-round table generation*/
            e.printStackTrace();
        }
    }
}
